
package at.fhj.ase.crypto.common.net;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility and convenience class that makes socket access and usage easier.
 * <p>
 * If a socket gets data in, {@link MessageHandler#handleMessage(String)} or {@link MessageHandler#handleError(String)} is called,<br>
 * if the developer wants to send data over socket, the {@link #sendMessage(String)} method can be called.
 * <p>
 * In the background, a {@link BufferedReader} and {@link BufferedWriter} is instantiated with the Socket's input- and outputstream.<br>
 * Those properties are then accessed e.g. when receiving data from the socket, or sending data over it.<br>
 * If an error occurs at socket communication, the socket connection is closed and this thread is interrupted.<br>
 * We can also say that this class stops reading/writing from the socket then.
 * <p>
 * The class is implemented as a {@link Thread} implementing {@link Runnable}, therefore it has to be started manually using {@link #start()}.<br>
 * As the class saves its running {@link Thread} instance (can only be one), you can also interrupt it again using {@link #stop()}.
 * 
 * @author sfe | www.fh-joanneum.at | Practical Aspects of Cryptography
 */
public final class SocketWrapperThread implements Runnable, MessageDispatcher {
  /** The {@link Logger} instance */
  private final Logger logger = LoggerFactory.getLogger(getClass().getName());
  /** The buffered reader which saved the {@link InputStream} of the socket */
  private final BufferedReader reader;
  /** The buffer writer which uses the {@link OutputStream} of the socket */
  private final BufferedWriter writer;
  /** The actual socket connection that is wrapped by this instance */
  private final Socket socket;
  /** The {@link MessageHandler} instance that is responsible for dealing with receiving messages */
  private final MessageHandler messageHandler;
  /** The running thread that is saved to be able to stop it again from outside (null as long as thread not started) */
  private Thread runningThread;

  /**
   * Constructor creating a new instance by wrapping the provided {@link Socket} connection.<br>
   * The {@link MessageHandler} must be implemented as this instance is responsible for arriving client messages. This class delegates all arriving messages on
   * the socket to this instance.
   * 
   * @param socket the socket that shall be wrapped by this class
   * @param messageHandler the {@link MessageHandler} implementation that is responsible for handling all arriving socket messages
   * @throws IOException if there occurs an exception at fetching the input- or the outputstream of the socket
   */
  public SocketWrapperThread(final Socket socket, final MessageHandler messageHandler) throws IOException {
    this.socket = socket;
    this.messageHandler = messageHandler;

    reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
    writer = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));
    logger.debug("Successfully initialized");
  }

  /** Starts this instance if it has not already been started (calls the {@link Thread#run()} method) */
  public void start() {
    if (runningThread == null) {
      runningThread = new Thread(this);
      runningThread.start();
      logger.debug("Thread successfully created and started started");
    } else {
      logger.warn("Thread already running, doing nothing");
    }
  }

  /** Stop this thread and close the socket, e.g. if an error at validation occurs (calls {@link Thread#interrupt()}) */
  public void stop() {
    if (runningThread != null && runningThread.isAlive() && !runningThread.isInterrupted()) {
      runningThread.interrupt();
      logger.debug("Thread successfully interrupted");
      closeSocket();
    } else {
      logger.warn("Thread not started");
    }
  }

  /**
   * Method listening on the open socket.<br>
   * If a message/line arrives, it is delegates to {@link MessageHandler#handleMessage(String)}.<br>
   * If an error occurs, {@link MessageHandler#handleError(String)} is called.
   * <p>
   * At an error, the instance tries to interrupt itself and close the socket. This means that it will no longer operate with the socket.
   */
  @Override
  public void run() {
    while (!runningThread.isInterrupted()) {
      try {
        messageHandler.handleMessage(reader.readLine());
      } catch (IOException e) {
        logger.error("Could not read line", e);
        runningThread.interrupt();
        closeSocket();
        messageHandler.handleError("Network error: " + e.toString());
      }
    }
  }

  /**
   * Closing the wrapped socket.<br>
   * Exceptions are caught and logged to logging instance.
   */
  private void closeSocket() {
    try {
      logger.debug("Trying to close socket...");
      socket.close();
    } catch (IOException e1) {
      logger.error("Could not close socket", e1);
    }
  }

  /**
   * Sends a message over the socket and its {@link OutputStream}.<br>
   * This is the implementation of {@link MessageDispatcher#sendMessage(String)}.<br>
   * Exceptions are called and logged to logging instance.
   */
  public void sendMessage(final String message) {
    logger.debug("Sending Message from thread '" + Thread.currentThread().getName() + "' > " + message);
    try {
      writer.write(message);
      writer.newLine();
      writer.flush();
    } catch (IOException e) {
      logger.error("Could not send message to thread '" + Thread.currentThread().getName() + "'", e);
    }
  }
}
